次に取るべき行動の一覧を表示するPage Menu
機能
押すとそのページへ飛ぶ
同じprojectの場合は同じページで開く
違うprojectの場合は新しいタブで開く
実装
更新機能は削る
そのうち実装するかも
実装したいこと
このUserScirptに対して以下の不満がある
全てのやることがでてきても嬉しくない
今日やるタスクや、1週間以内にやるタスクなどに絞って閲覧したい
🚧などがでてこない
このUserScriptはあくまで試しに作ってみた程度だから、これは別のuserscriptとして改めて作るべきかな
code:script.js
import { setup } from "./mod.js";
code:mod.js
var l=(e,t)=>{if(!(e instanceof HTMLDivElement))throw new TypeError("${t}" must be HTMLDivElememt but actual is "${e}")};var m=()=>w(document.getElementsByClassName("status-bar")?.0,"div.status-bar"),w=(e,t)=>{if(!!e)return l(e,t),e};function p(){let e=m();if(!e)throw new Error("div.status-bar can't be found");let t=document.createElement("div");return e.append(t),{render:(...n)=>{t.textContent="";let r=g(...n);r&&t.append(r)},dispose:()=>t.remove()}}function g(...e){let t=e.flatMap(r=>{switch(r.type){case"spinner":returnk();case"check-circle":returnC();case"exclamation-triangle":returnv();case"text":returna(r.text);case"group":{let o=g(...r.items);return o?o:[]}}});if(t.length===0)return;if(t.length===1)return t0;let n=document.createElement("span");return n.classList.add("item-group"),n.append(...t),n}function a(e){let t=document.createElement("span");return t.classList.add("item"),t.append(e),t}function k(){let e=document.createElement("i");return e.classList.add("fa","fa-spinner"),a(e)}function C(){let e=document.createElement("i");return e.classList.add("kamon","kamon-check-circle"),a(e)}function v(){let e=document.createElement("i");return e.classList.add("fas","fa-exclamation-triangle"),a(e)}var u=e=>...e.map((t,n)=>t===" "?"_":!D.includes(t)||n===e.length-1&&I.includes(t)?encodeURIComponent(t):t).join(""),D='@$&+=:;",',I=':;",';function x(e,t,n){let r=document.createElement("a");r.href=/${e}/${u(t)}${typeof n!="string"?"":?body=${encodeURIComponent(n)}},document.body.append(r),r.click(),r.remove()}var i="next-action",h,A="/assets/img/favicon/apple-touch-icon.png";function rt(e){let t=head style[data-userscript-name="${i}"];document.querySelector(t)?.remove?.();let n=document.createElement("style");n.dataset.userscriptName=i,n.textContent=`a#${i}.tool-btn:hover { text-decoration: none;
}
a#${i}.tool-btn::before {
position: absolute;
content: "\\f0ae";
font: 900 20px/46px "Font Awesome 5 Free";
}
a#${i}.tool-btn img {
opacity: 0;
}
a#${i}.tool-btn ~ ul a::before {
position: absolute;
font-family: "Font Awesome 5 Free";
font-weight: 900;
}
a#${i}.tool-btn ~ ul img {
opacity: 0;
margin-right: 0;
},document.head.append(n),document.getElementById(i)||scrapbox.PageMenu.addMenu({title:i,image:A,onClick:async()=>{h??=B(e),await h}})}async function B(e){scrapbox.PageMenu(i).removeAllItems();let{render:t,dispose:n}=p(),r=0;try{for(let o of e){t({type:"spinner"},{type:"text",text:Searching "/${o}" for next actions...});for await(let s of K(o))r++,scrapbox.PageMenu(i).addItem({title:s,onClick:()=>{let c=https://scrapbox.io/${o}/${u(s)};if(o!==scrapbox.Project.name){window.open(c);return}x(c)}});o!==e[e.length-1]&&scrapbox.PageMenu(i).addSeparator()}t({type:"check-circle"},{type:"text",text:Found ${r} actions.})}catch(o){t({type:"exclamation-triangle"},{type:"text",text:o instanceof Error?${o.name} ${o.message}:"Unknown error! (see developper console)"}),console.error(o)}finally{setTimeout(()=>n(),1e3)}}async function*K(e,t){if(t??=/^(?:⬜(?:[^p]*|p[^-]*))|🔳/,e===scrapbox.Project.name){for(let{title:r,exists:o}of scrapbox.Project.pages)!t.test(r)||(yield r);return}let n=new Set;for await(let r of N(e))!n.has(r)&&t.test(r)&&(n.add(r),yield r)}async function*N(e){let t=/api/pages/${e}/search/titles,n=null;do{let r=${t}${n??followingId=${n}:""}`,o=await fetch(r);n=o.headers.get("X-following-id");let s=await o.json();for(let{title:c,links:E}of s){yield c;for(let y of E)yield y}if(!n)break}while(!0)}export{rt as setup}; code:mod.ts
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
import {
useStatusBar,
openInTheSameTab,
encodeTitleURI,
} from "../scrapbox-userscript-std/dom.ts";
declare const scrapbox: Scrapbox;
const id = "next-action";
let initialized: Promise<void>;
const dummyImage = "/assets/img/favicon/apple-touch-icon.png";
export function setup(projects: string[]) {
const selector = head style[data-userscript-name="${id}"];
document.querySelector(selector)?.remove?.();
const style = document.createElement("style");
style.dataset.userscriptName = id;
style.textContent = `a#${id}.tool-btn:hover {
text-decoration: none;
}
a#${id}.tool-btn::before {
position: absolute;
content: "\\f0ae";
font: 900 20px/46px "Font Awesome 5 Free";
}
a#${id}.tool-btn img {
opacity: 0;
}
a#${id}.tool-btn ~ ul a::before {
position: absolute;
font-family: "Font Awesome 5 Free";
font-weight: 900;
}
a#${id}.tool-btn ~ ul img {
opacity: 0;
margin-right: 0;
}`;
document.head.append(style);
if (!document.getElementById(id)) {
scrapbox.PageMenu.addMenu({
title: id,
image: dummyImage,
onClick: async () => {
initialized ??= load(projects);
await initialized;
},
});
}
}
code:mod.ts
async function load(projects: string[]) {
scrapbox.PageMenu(id).removeAllItems();
const { render, dispose } = useStatusBar();
let count = 0;
try {
for (const project of projects) {
render(
{ type: "spinner" },
{ type: "text", text: Searching "/${project}" for next actions...},
);
for await (const title of listNextActions(project)) {
count++;
scrapbox.PageMenu(id).addItem({
title,
onClick: () => {
project
}/${encodeTitleURI(title)}`;
if (project !== scrapbox.Project.name) {
window.open(path);
return;
}
openInTheSameTab(path);
},
});
}
scrapbox.PageMenu(id).addSeparator();
}
render(
{ type: "check-circle" },
{ type: "text", text: Found ${count} actions.},
);
} catch(e: unknown) {
render(
{ type: "exclamation-triangle" },
{ type: "text", text: e instanceof Error ?
${e.name} ${e.message} :
Unknown error! (see developper console),
},
);
console.error(e);
} finally {
setTimeout(() => dispose(), 1000);
}
}
code:mod.ts
async function* listNextActions(project: string, filter?: RegExp) {
filter ??= /^(?:⬜(?:^p*|p^-*))|🔳/; if (project === scrapbox.Project.name) {
for (const { title, exists } of scrapbox.Project.pages) {
if (!filter.test(title)) continue;
yield title;
}
return;
}
const titles = new Set<string>();
for await (const title of getLinks(project)) {
if (!titles.has(title) && filter.test(title)) {
titles.add(title);
yield title;
}
}
}
async function* getLinks(project: string) {
const path = /api/pages/${project}/search/titles;
let followingId = null;
do {
const path_ = `${path}${
followingId ? ?followingId=${followingId} : ""
}` as string;
const res = await fetch(path_);
followingId = res.headers.get("X-following-id");
const pages = (await res.json()) as SearchedTitle[];
for (const { title, links } of pages) {
yield title;
for (const link of links) {
yield link;
}
}
if (!followingId) break; // 空文字列の場合もある
} while (true)
}